{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# SOLT"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This little example demonstrates how to use scikit-rf for a basic SOLT calibration. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Basic Use \n",
    "\n",
    "### Imports \n",
    "\n",
    "First we import `skrf`, the `SOLT` class, and setup matplotlib plotting. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import skrf as rf\n",
    "\n",
    "rf.stylely()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Typical SOLT Procedure"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A two-port calibration is accomplished in an identical way to one-port. However, scikit-rf only accepts measurement results as two-port networks, even for reflective and load standards."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Measure Two Standards Separately\n",
    "\n",
    "If you don't have two sets of calibration standards, no worries! It's possible to measure reflective standards on each port separately. For example, connect a short to port-1, save a one-port measurement as `short.s1p`, then move the same short to port-2, save another one-port measurement as `short2.s1p` or similar.\n",
    "\n",
    "Next, you can forge a two-port Network from two one-port Network using the function `skrf.network.two_port_reflect`:\n",
    "\n",
    "    short = rf.Network('ideals/short.s1p')  # a 1-port Network\n",
    "    shorts = rf.two_port_reflect(short, short)  # a 2-port Network\n",
    "\n",
    "The function `skrf.network.two_port_reflect` does this:\n",
    "\n",
    "<img src=\"2_1port_to_1_2port.svg\" width=\"50%\"/>\n",
    "\n",
    "#### Measure Two Standards Simultaneously\n",
    "\n",
    "Often, it's possible to measure two reflective standards simultaneously, then to directly store your data as a two-port network. For example, connect the first short standard to port-1 and a second short to port-2, and save everything as a two-port measurement, such as `short,short.s2p` or similar.\n",
    "\n",
    "<img src=\"VNA_2_1port.svg\" width=\"30%\"/>\n",
    "\n",
    "#### Isolation Calibration\n",
    "\n",
    "For Open, Short, and Load calibrations, even though the inputs are supplied to scikit-rf as 2-port networks with four S-parameters in each, $S_{21}$, and $S_{12}$ are ignored - by definition, both are equal to 0. Only $S_{11}$ and $S_{22}$ are used, this is true even for a Load-Load measurement. Thus, no isolation calibration takes place by default.\n",
    "\n",
    "If isolation calibration is needed, one must explicitly tell scikit-rf to do so. Specifically, `SOLT()` class accepts an optional parameter `isolation` for this purpose. To calibrate isolation, set this parameter to a two-port network, representing the measurement result with loads on both ports."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Code\n",
    "\n",
    "The typical workflow for a SOLT calibration is:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "    # a list of Network types, holding 'ideal' responses\n",
    "    my_ideals = [\n",
    "\t    rf.Network('ideal/short, short.s2p'),\n",
    "\t    rf.Network('ideal/open, open.s2p'),\n",
    "\t    rf.Network('ideal/load, load.s2p'),\n",
    "\t    rf.Network('ideal/thru.s2p'),\n",
    "        ]\n",
    "    \n",
    "    # a list of Network types, holding 'measured' responses\n",
    "    my_measured = [\n",
    "\t    rf.Network('measured/short, short.s2p'),\n",
    "\t    rf.Network('measured/open, open.s2p'),\n",
    "\t    rf.Network('measured/load, load.s2p'),\n",
    "\t    rf.Network('measured/thru.s2p'),\n",
    "\t    ]\n",
    "        \n",
    "    ## create a SOLT instance\n",
    "    cal = SOLT(\n",
    "\t    ideals = my_ideals,\n",
    "\t    measured = my_measured,\n",
    "        isolation = rf.Network('measured/load, load.s2p'),\n",
    "        # isolation calibration is optional, it can be removed.\n",
    "\t    )\n",
    "        \n",
    "    ## run, and apply calibration to a DUT\n",
    "    # run calibration algorithm\n",
    "    cal.run() \n",
    "    \n",
    "    # apply it to a dut\n",
    "    dut = rf.Network('my_dut.s2p')\n",
    "    dut_caled = cal.apply_cal(dut)\n",
    "    \n",
    "    # plot results\n",
    "    dut_caled.plot_s_db()\n",
    "    # save results \n",
    "    dut_caled.write_touchstone()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example\n",
    "The following example illustrates a common situation: a DUT is connected to a VNA using two cables of different lengths. The purpose of the calibration is to move the reference plane do the DUT, that is to remove the effect of the cables from the measurement.\n",
    "\n",
    "<img src=\"line1_dut_line2.svg\" width=\"60%\"/>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the example below, the DUT is already known, just to be able to confirm that the calibration method is working at the end. Of course, in reality, the DUT is generally not known..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dut = rf.data.ring_slot\n",
    "dut.plot_s_db(lw=2)  # this is what we should find after the calibration"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The ideal component Networks are obtained from your calibration kit manufacturers or from modelling.\n",
    "\n",
    "In this example, we simulate ideal components from transmission line theory. We create a lossy and noisy transmission line (for the sake of the example)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "media = rf.DefinedGammaZ0(frequency=dut.frequency, gamma=0.5 + 1j)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then we create the ideal components: Short, Open and Load, and Through. By default, the methods `media.short()`, `media.open()`, and `media.match()` return a one-port network, the SOLT class expects a list of two-port Network, so `two_port_reflect()` is needed to forge a two-port network from two one-port networks (`media.thru()` returns a two-port network and no adjustment is needed).\n",
    "\n",
    "Alternatively, the argument `nports=2` can be used as a shorthand for this task."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ideal 1-port Networks\n",
    "short_ideal = media.short()\n",
    "open_ideal = media.open()\n",
    "load_ideal = media.match()  # could also be: media.load(Gamma0=0)\n",
    "thru_ideal = media.thru()\n",
    "\n",
    "# forge a two-port network from two one-port networks\n",
    "short_ideal_2p = rf.two_port_reflect(short_ideal, short_ideal)\n",
    "open_ideal_2p = rf.two_port_reflect(open_ideal, open_ideal)\n",
    "load_ideal_2p = rf.two_port_reflect(load_ideal, load_ideal)\n",
    "\n",
    "# alternatively, the \"nport=2\" argument can be used as a shorthand\n",
    "# short_ideal_2p = media.short(nports=2)\n",
    "# open_ideal_2p = media.open(nports=2)\n",
    "# load_ideal_2p = media.match(nports=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that we have our ideal elements, let's fake the measurements.\n",
    "\n",
    "Note that the transmission lines are not symmetric in the example below, to make it as generic as possible. In such case, it is necessary to call the [flipped()](../../api/generated/skrf.network.Network.flipped.rst) method to connect the ideal elements on the correct side of the `line2` object. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# left and right piece of transmission lines\n",
    "line1 = media.line(d=20, unit='cm')**media.impedance_mismatch(1,2)\n",
    "line2 = media.line(d=30, unit='cm')**media.impedance_mismatch(1,3)\n",
    "\n",
    "# add some noise to make it more realistic\n",
    "line1.add_noise_polar(.01, .1)\n",
    "line2.add_noise_polar(.01, .1)\n",
    "\n",
    "# fake the measured setup\n",
    "measured = line1 ** dut  ** line2\n",
    "\n",
    "# fake the calibration measurements\n",
    "# Note the use of flipped() on line2\n",
    "open_measured = rf.two_port_reflect(line1 ** media.open(), line2.flipped() ** media.open())\n",
    "short_measured = rf.two_port_reflect(line1 ** media.short(), line2.flipped() ** media.short())\n",
    "load_measured = rf.two_port_reflect(line1 ** media.load(Gamma0=0), line2.flipped() ** media.load(Gamma0=0))\n",
    "thru_measured = line1 ** line2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now create the lists of Network that the `SOLT` class expects:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# a list of Network types, holding 'ideal' responses\n",
    "my_ideals = [\n",
    "    short_ideal_2p,\n",
    "    open_ideal_2p,\n",
    "    load_ideal_2p,\n",
    "    thru_ideal,   # Thru should be the last\n",
    "    ]\n",
    "\n",
    "# a list of Network types, holding 'measured' responses\n",
    "my_measured = [\n",
    "    short_measured,\n",
    "    open_measured,\n",
    "    load_measured,\n",
    "    thru_measured,   # Thru should be the last\n",
    "]\n",
    "\n",
    "## create a SOLT instance\n",
    "cal = rf.calibration.SOLT(\n",
    "    ideals = my_ideals,\n",
    "    measured = my_measured,\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And finally apply the calibration:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# run calibration algorithm\n",
    "cal.run()\n",
    "\n",
    "# apply it to a dut\n",
    "measured_caled = cal.apply_cal(measured)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's see the results for S11 and S21:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "measured.plot_s_db(m=0, n=0, lw=2, label='measured')\n",
    "measured_caled.plot_s_db(m=0, n=0, lw=2, label='caled')\n",
    "dut.plot_s_db(m=0, n=0, ls='--', lw=2, label='expected')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "measured.plot_s_db(m=1, n=0, lw=2, label='measured')\n",
    "measured_caled.plot_s_db(m=1, n=0, lw=2, label='caled')\n",
    "dut.plot_s_db(m=1, n=0, ls='--', lw=2, label='expected')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The caled Network is (mostly) equal the DUT as expected:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dut == measured"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dut == measured_caled  # within 1e-4 absolute tolerance"
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
